﻿using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Uow;

namespace Xms.Abp.Caching;

public class HashCache<TCacheItem> : IHashCache<TCacheItem>
{
    protected IAbpLazyServiceProvider LazyServiceProvider { get; }

    protected ICacheProvider CacheProvider { get; }
    protected IXmsCache _cache;
    protected IXmsCache Cache
    {
        get
        {
            if (_cache == null)
            {
                _cache = CacheProvider.GetCache();
                var value = _cache.Get(CacheKey);
                if (value == null)
                {
                    using (SyncSemaphore.Lock())
                    {
                        AsyncHelper.RunSync(async () =>
                        {
                            if (await _cache.KeyExistsAsync(CacheKey))
                            {
                                await _cache.SetAsync(CacheKey, Serializer.Serialize("HASH"), CacheOptions);
                            }
                        });
                    }
                }
            }
            return _cache;
        }
    }

    protected ILogger<HashCache<TCacheItem>> Logger => LazyInject<ILogger<HashCache<TCacheItem>>>();

    protected ICancellationTokenProvider CancellationTokenProvider => LazyInject<ICancellationTokenProvider>();

    protected IDistributedCacheSerializer Serializer => LazyInject<IDistributedCacheSerializer>();

    protected IServiceScopeFactory ServiceScopeFactory => LazyInject<IServiceScopeFactory>();

    protected IUnitOfWorkManager UnitOfWorkManager => LazyInject<IUnitOfWorkManager>();

    protected SemaphoreSlim SyncSemaphore { get; } = new SemaphoreSlim(1, 1);

    protected bool IgnoreMultiTenancy { get; set; } = false;

    protected DistributedCacheEntryOptions CacheOptions { get; }

    protected ICurrentTenant CurrentTenant => LazyInject<ICurrentTenant>();

    protected AbpDistributedCacheOptions DistributedCacheOption
        => LazyInject<IOptions<AbpDistributedCacheOptions>>().Value;

    public bool MultiTenancyIsEnable { get => !IgnoreMultiTenancy; set => IgnoreMultiTenancy = !value; }

    protected string CacheKey { get; set; }
    protected string CacheName { get; set; }

    public HashCache(
        IAbpLazyServiceProvider lazyServiceProvider,
        ICacheProvider cacheProvider,
        string cacheName = null)
    {
        LazyServiceProvider = lazyServiceProvider;
        CacheProvider = cacheProvider;
        CacheOptions = LazyInject<IOptions<DistributedCacheEntryOptions<TCacheItem>>>().Value;

        if (CacheOptions == null || !CacheOptions.AbsoluteExpiration.HasValue && !CacheOptions.SlidingExpiration.HasValue && !CacheOptions.AbsoluteExpirationRelativeToNow.HasValue)
        {
            CacheOptions = GetDefaultCacheEntryOptions();
            if (CacheOptions == null)
            {
                CacheOptions = LazyInject<IOptions<DistributedCacheEntryOptions>>().Value;
            }
        }

        CacheName = cacheName ?? CacheNameAttribute.GetCacheName(typeof(TCacheItem));

        CacheKey = LazyInject<IDistributedCacheKeyNormalizer>()
            .NormalizeKey(new DistributedCacheKeyNormalizeArgs(null, CacheName, IgnoreMultiTenancy));
    }

    public TService LazyInject<TService>()
    {
        return LazyServiceProvider.LazyGetRequiredService<TService>();
    }

    protected virtual async Task HandleExceptionAsync(Exception ex)
    {
        Logger.LogException(ex, LogLevel.Warning);

        using var scope = ServiceScopeFactory.CreateScope();
        await scope.ServiceProvider
            .GetRequiredService<IExceptionNotifier>()
            .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
    }

    public async Task RefreshAsync(
        bool? hideErrors = null,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= DistributedCacheOption.HideErrors;

        try
        {
            await Cache.RefreshAsync(CacheKey, CancellationTokenProvider.FallbackToProvider(cancellationToken));
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }

            throw;
        }
    }

    public async Task RemoveAsync(
        bool? hideErrors = null,
        CancellationToken cancellationToken = default)
    {
        hideErrors ??= DistributedCacheOption.HideErrors;

        try
        {
            await Cache.RemoveAsync(CacheKey, CancellationTokenProvider.FallbackToProvider(cancellationToken));
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }

            throw;
        }
    }

    public async Task<bool> ExistsAsync(CancellationToken cancellationToken = default)
    {
        return await Cache.KeyExistsAsync(CacheKey, CancellationTokenProvider.FallbackToProvider(cancellationToken));
    }

    public async Task<DistributedCacheEntryOptions> GetExpirationFromCache(CancellationToken cancellationToken = default)
    {
        return await Cache.GetExpirationFromCache(CacheKey, CancellationTokenProvider.FallbackToProvider(cancellationToken));
    }



    protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions()
    {
        foreach (var configure in DistributedCacheOption.CacheConfigurators)
        {
            var options = configure.Invoke(CacheName);
            if (options != null)
            {
                return options;
            }
        }

        return DistributedCacheOption.GlobalCacheEntryOptions;
    }

    protected virtual bool ShouldConsiderUow(bool considerUow)
    {
        return considerUow && UnitOfWorkManager.Current != null;
    }

    protected virtual string GetUnitOfWorkCacheKey()
    {
        return "HashCache:" + CacheKey;
    }

    [CanBeNull]
    protected virtual TCacheItem ToCacheItem([CanBeNull] byte[] bytes)
    {
        if (bytes == null)
        {
            return default;
        }

        return Serializer.Deserialize<TCacheItem>(bytes);
    }

    protected virtual Dictionary<string, Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>>> GetUnitOfWorkHashCache()
    {
        if (UnitOfWorkManager.Current == null)
        {
            throw new AbpException($"There is no active UOW.");
        }

        return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(),
            key => new Dictionary<string, Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>>>());
    }

    protected virtual Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache(string key)
    {
        var cache = GetUnitOfWorkHashCache();
        return cache.GetOrAdd(key, k => new Dictionary<string, XmsUnitOfWorkCacheItem<TCacheItem>>());
    }

    public async Task<bool> FieldExsitsAsync(string field, CancellationToken cancellationToken = default)
    {
        return await Cache.HashFieldExsitsAsync(CacheKey, field, cancellationToken);
    }

    public async Task<TCacheItem> GetAsync([NotNull] string field, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        hideErrors ??= DistributedCacheOption.HideErrors;

        if (ShouldConsiderUow(considerUow))
        {
            var value = GetUnitOfWorkCache(CacheKey).GetOrDefault(field).GetUnRemovedValueOrNull();
            if (value != null)
            {
                return value;
            }

        }

        try
        {
            byte[] cachedBytes = await Cache.GetHashAsync(CacheKey, field, cancellationToken);
            await RefreshAsync();
            return ToCacheItem(cachedBytes);
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return default;
            }

            throw;
        }
    }

    private static KeyValuePair<string, TCacheItem>[] ToCacheItemsWithDefaultValues(string[] fields)
    {
        return fields
            .Select(field => new KeyValuePair<string, TCacheItem>(field, default))
            .ToArray();
    }

    protected virtual KeyValuePair<string, TCacheItem>[] ToCacheItems(byte[][] itemBytes, string[] itemFields)
    {
        if (itemBytes.Length != itemFields.Length)
        {
            throw new AbpException("count of the item bytes should be same with the count of the given keys");
        }

        var result = new List<KeyValuePair<string, TCacheItem>>();

        for (int i = 0; i < itemFields.Length; i++)
        {
            result.AddIfNotContains(
                new KeyValuePair<string, TCacheItem>(
                    itemFields[i],
                    ToCacheItem(itemBytes[i])
                )
            );
        }

        return result.ToArray();
    }

    public async Task<KeyValuePair<string, TCacheItem>[]> GetManyAsync(IEnumerable<string> fields, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        var fieldArray = fields.ToArray().Distinct().ToArray();
        //var fieldArray = fields.ToArray();

        var notCachedFields = new List<string>();
        var cachedValues = new List<KeyValuePair<string, TCacheItem>>();
        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache(CacheKey);
            foreach (var field in fieldArray)
            {
                var value = uowCache.GetOrDefault(field).GetUnRemovedValueOrNull();
                if (value != null)
                {
                    cachedValues.Add(new KeyValuePair<string, TCacheItem>(field, value));
                }
            }

            notCachedFields = fieldArray.Except(cachedValues.Select(x => x.Key)).ToList();
            if (!notCachedFields.Any())
            {
                return cachedValues.ToArray();
            }
        }

        hideErrors ??= DistributedCacheOption.HideErrors;
        byte[][] cachedBytes;

        var readFields = notCachedFields.Any() ? notCachedFields.ToArray() : fieldArray;
        try
        {
            var result = await Cache.GetHashManyAsync(CacheKey, readFields, cancellationToken);
            cachedBytes = result.Select(r => r.Value).ToArray();
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return ToCacheItemsWithDefaultValues(fieldArray);
            }

            throw;
        }

        return cachedValues.Concat(ToCacheItems(cachedBytes, readFields)).ToArray();
    }

    public async Task<KeyValuePair<string, TCacheItem>[]> GetAllAsync(bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        try
        {
            var result = (await Cache.GetHashAllAsync(CacheKey, cancellationToken)).ToDictionary(r => r.Key, r => ToCacheItem(r.Value));
            if (ShouldConsiderUow(considerUow))
            {
                var uowCache = GetUnitOfWorkCache(CacheKey);
                foreach (var field in result.Keys)
                {
                    var item = uowCache.GetOrDefault(field);
                    var value = item.GetUnRemovedValueOrNull();
                    if (value != null)
                    {
                        result[field] = value;
                    }
                }
            }
            return result.ToArray();
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return Array.Empty<KeyValuePair<string, TCacheItem>>();
            }

            throw;
        }
    }

    public async Task<TCacheItem> GetOrAddAsync([NotNull] string field, Func<Task<TCacheItem>> factory, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        var value = await GetAsync(field, hideErrors, considerUow, cancellationToken);
        if (value != null)
        {
            return value;
        }

        using (await SyncSemaphore.LockAsync(cancellationToken))
        {
            value = await GetAsync(field, hideErrors, considerUow, cancellationToken);
            if (value != null)
            {
                return value;
            }

            value = await factory();

            if (ShouldConsiderUow(considerUow))
            {
                var uowCache = GetUnitOfWorkCache(CacheKey);
                if (uowCache.TryGetValue(field, out var item))
                {
                    item.SetValue(value);
                }
                else
                {
                    uowCache.Add(field, new XmsUnitOfWorkCacheItem<TCacheItem>(value));
                }
            }

            await SetAsync(field, value, hideErrors, considerUow, cancellationToken);
        }

        return value;
    }

    public async Task RemoveAsync([NotNull] string field, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        async Task RemoveRealCache()
        {
            cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
            hideErrors ??= DistributedCacheOption.HideErrors;
            try
            {
                await Cache.RemoveHashAsync(CacheKey, field, cancellationToken);
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                }
                else
                {
                    throw;
                }
            }
            await RefreshAsync(hideErrors, cancellationToken);
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache(CacheKey);
            if (uowCache.TryGetValue(field, out _))
            {
                uowCache[field].RemoveValue();
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
        }
        else
        {
            await RemoveRealCache();
        }
    }

    public async Task SetAsync([NotNull] string field, [NotNull] TCacheItem value, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        async Task SetRealCache()
        {
            cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
            hideErrors ??= DistributedCacheOption.HideErrors;
            try
            {
                await Cache.SetHashAsync(CacheKey, field, Serializer.Serialize(value), cancellationToken);
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                }
                else
                {
                    throw;
                }
            }
            await RefreshAsync(hideErrors, cancellationToken);
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache(CacheKey);
            if (uowCache.TryGetValue(field, out _))
            {
                uowCache[field].SetValue(value);
            }
            else
            {
                uowCache.Add(field, new XmsUnitOfWorkCacheItem<TCacheItem>(value));
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(SetRealCache);
        }
        else
        {
            await SetRealCache();
        }
    }

    public async Task SetManyAsync(IEnumerable<KeyValuePair<string, TCacheItem>> items, bool? hideErrors = null, bool considerUow = false, CancellationToken cancellationToken = default)
    {
        async Task SetRealCache()
        {
            cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
            hideErrors ??= DistributedCacheOption.HideErrors;
            try
            {
                await Cache.SetHashManyAsync(CacheKey, items.Select(r => new KeyValuePair<string, byte[]>(r.Key, Serializer.Serialize(r.Value))), cancellationToken);
            }
            catch (Exception ex)
            {
                if (hideErrors == true)
                {
                    await HandleExceptionAsync(ex);
                }
                else
                {
                    throw;
                }
            }
            await RefreshAsync(hideErrors, cancellationToken);
        }

        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache(CacheKey);

            var itemsArray = items.ToArray();
            foreach (var pair in itemsArray)
            {
                if (uowCache.TryGetValue(pair.Key, out _))
                {
                    uowCache[pair.Key].SetValue(pair.Value);
                }
                else
                {
                    uowCache.Add(pair.Key, new XmsUnitOfWorkCacheItem<TCacheItem>(pair.Value));
                }
            }

            // ReSharper disable once PossibleNullReferenceException
            UnitOfWorkManager.Current.OnCompleted(SetRealCache);
        }
        else
        {
            await SetRealCache();
        }
    }

    public async Task<IEnumerable<string>> GetFieldsAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken = CancellationTokenProvider.FallbackToProvider(cancellationToken);
        return await Cache.GetHashKeysAsync(CacheKey, cancellationToken);
    }
}
